package roborally;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Raw;

/**
 * A class of nodes, that contains a G-score, H-score, F-score, parent node, energy to reach a position, orientation and list with all nodes.
 * This class is used by the ShortestPathAlgorithm class that calculates the most efficient path between 2 points (nodes), avoiding obstacles.
 * 
 * @invar  | Coordinate.isValidNodeCoordinate(this.getCoordinate())
 * @invar  | isValidGScore(this.getGScore())
 * @invar  | isValidHScore(this.getHScore())
 * @invar  | fScore == this.getGScore() + this.getHScore()
 * @invar  | isValidEnergyToReach(this.getEnergyToReach())
 * @invar  | Orientation.isValidOrientation(this.getOrientation()) 
 * 
 * @version  2.90
 * @author   Michiel Ruelens & Dieter Naten
 */
public class Node {
	
	private final Coordinate coordinate;
	private Long gScore; //The number of steps needed to reach this node
	private Long hScore; //The used heuristic, in our case: Manhattan distance
	private Long fScore; //gScore + hScore
	private Node parentNode; //Reference to it's parent node to reconstruct the path
	private Long energyToReach;
	private final Orientation orientation; //The orientation a robot would have if it followed the shortest path to this node.
	private static ArrayList<Node> nodeList = new ArrayList<Node>(); //Array that keeps all existing nodes so no copies are made
	
	/**
	 * Constructor that initializes the node with the given parameters. 
	 * This constructor is private cause every node can only exist once.
	 * 
	 * @param c
	 * 		  The coordinate the new Node should get
	 * @param g
	 * 		  The G-Score the new Node should get
	 * @param h
	 * 		  The H-Score the new node should get
	 * @param energyToReach
	 *        The energy to reach of the new node
	 * @param parent
	 * 		  The parent node of the new node
	 * @param orientation
	 *        The orientation of the new node
	 * @pre   | isValidGScore(g)
	 * @pre   | isValidHScore(h)
	 * @pre   | isValidEnergyToReach(energyToReach)
	 * @pre   | Orientation.isValidOrientation(orientation)
	 * @pre   | Coordinate.isValidNodeCoordinate(c)
	 * @pre   | (parent instanceof Node || parent == null)
	 * @post  | (new this).getCoordinate() == coordinate
	 * @post  | (new this).getGScore() == g
	 * @post  | (new this).getEnergyToReach == energyToReach
	 * @post  | (new this).getParentNode == parent
	 * @post  | (new this).getOrientation == orientation
	 */
	@Raw
	private Node(Coordinate c, Long g, Long h, Long energyToReach, Node parent, Orientation orientation) {
		assert(Coordinate.isValidNodeCoordinate(c));
		assert(isValidGScore(g));
		assert(isValidHScore(h));
		assert(isValidEnergyToReach(energyToReach));
		assert(parent instanceof Node || parent == null);
		assert(Orientation.isValidOrientation(orientation));
		this.coordinate = c;
		this.gScore = g;
		this.hScore = h;
		this.fScore = getGScore()+getHScore();
		this.energyToReach = energyToReach;
		this.parentNode = parent;
		this.orientation = orientation;
	}

	/**
	 * Create a new Node, if it already exists return the existing one.
	 * The new Node is created using the private constructor.
	 * 
	 * @param  c
	 * 		   The coordinate the new Node should get
	 * @param  g
	 * 		   The G-Score the new Node should get
	 * @param  h
	 * 		   The H-Score the new node should get
	 * @param  energyToReach
	 *         The energy to reach the new node from another start node
	 * @param  parent
	 * 		   The parent node of the new node
	 * @param  orientation
	 *         The orientation of the new node
	 * @pre    | isValidGScore(g)
	 * @pre    | isValidHScore(h)
	 * @pre    | isValidEnergyToReach(energyToReach)
	 * @pre    | Orientation.isValidOrientation(orientation)
	 * @pre    | Coordinate.isValidNodeCoordinate(c)
	 * @pre    | (parent instanceof Node || parent == null)
	 * @effect | new Node(c,g,h, energyToReach, parent, orientation)
	 */
	public static Node getNode(Coordinate c, Long g, Long h, Long energyToReach, Node parent, Orientation orientation) {
		assert(Coordinate.isValidNodeCoordinate(c));
		assert(isValidGScore(g));
		assert(isValidHScore(h));
		assert(isValidEnergyToReach(energyToReach));
		assert(parent instanceof Node || parent == null);
		assert(Orientation.isValidOrientation(orientation));
		Node resultNode = null;
		if(!nodeList.isEmpty()){
			for(Node n:nodeList){
				if(n.getCoordinate().equals(c)){
					resultNode = n;
				}
			}
		}
		if(resultNode==null){
			resultNode = new Node(c,g,h, energyToReach, parent, orientation);
			nodeList.add(resultNode);
		}
		return resultNode;
	}
	
	/**
	 * Returns the coordinate of the current node.
	 */
	@Basic
	public Coordinate getCoordinate() {
		return coordinate;
	}
	
	/**
	 * Returns the G-Score of the current node.
	 */
	@Basic
	public Long getGScore() {
		return gScore;
	}
	
	/**
	 * Changes the G-Score to the given value
	 * 
	 * @param gScore
	 *        The new value the G-Score should be given.
	 * @pre   | isValidGScore(gScore)
	 * @post  | (new this).getGScore() == gScore
	 * @post  | (new this).getFscore() == this.getHscore() + gScore
	 */
	public void changeGScore(Long gScore){
		assert(isValidGScore(gScore));
		this.gScore = gScore;
		this.fScore = getGScore()+getHScore();
	}
	
	/**
	 * This method verifies wheter the given G-Score is valid.
	 * 
	 * @param  g
	 *         The G-score to be checked
	 * @return | ((0<=g) && g<=Long.MAX_VALUE)
	 */
	public static boolean isValidGScore(long g){
		return ((0<=g) && g<=Long.MAX_VALUE);
	}
	
	/**
	 * Returns the H-Score of the current node.
	 */
	@Basic
	public Long getHScore() {
		return hScore;
	}
	
	/**
	 * Changes the H-Score to the given value
	 * 
	 * @param hScore
	 *        The new value the H-Score should be given.
	 * @pre   | isValidHScore(hScore)
	 * @post  | (new this).getHScore() == hScore
	 * @post  | (new this).getFscore() == this.getGscore() + hScore
	 */
	public void changeHScore(Long hScore){
		assert(isValidHScore(hScore));
		this.hScore = hScore;
		this.fScore = getGScore()+getHScore();
	}

	/**
	 * This method verifies wheter the given H-Score is valid.
	 * 
	 * @param g
	 *        The G-score to be checked
	 * @return | ((0<=h) && h<=Long.MAX_VALUE)
	 */
	public static boolean isValidHScore(long h){
		return ((0<=h) && h<=Long.MAX_VALUE);
	}
	
	/**
	 * Returns the F-Score of the current node.
	 */
	@Basic
	public Long getFScore() {
		return fScore;
	}
	
	/**
	 * Returns the energy to reach of the current node.
	 */
	@Basic
	public Long getEnergyToReach() {
		return energyToReach;
	}
	
	/**
	 * Change the value of the field 'energyToReach' to the given value.
	 * 
	 * @param energyAmount
	 *        The 'energyToReach' the current node should get
	 * @pre   | isValidEnergyToReach(energyAmount)
	 * @post  | (new this).getEnergyToReach() == energyAmount
	 */
	public void setEnergyToReach(Long energyAmount) {
		assert(isValidEnergyToReach(energyAmount));
		this.energyToReach = energyAmount;
	}
	
	/**
	 * This method verifies wheter the given energy is valid for the 'energyToReach' field.
	 * 
	 * @param  energyTR
	 *         The energy to be checked
	 * @return | (energyTR>=0 && energyTR<=Long.MAX_VALUE)
	 */
	private static boolean isValidEnergyToReach(Long energyTR) {
		return (energyTR>=0 && energyTR<=Long.MAX_VALUE);
	}
	
	/**
	 * Returns the parent node of the current node.
	 */
	@Basic
	public Node getParentNode() {
		return parentNode;
	}
	
	/**
	 * Changes the parent node of the current node to the given node.
	 * 
	 * @param parent
	 *        The node 
	 * @pre   | (parent instanceof Node || parent == null)
	 * @post  | (new this).getParentNode() == parent
	 */
	public void changeParentNode(Node parent) {
		assert(parent instanceof Node || parent == null);
		this.parentNode = parent;
	}
	
	/**
	 * Returns the orientation of the current node.
	 */
	@Basic
	public Orientation getOrientation()	{
		return orientation;
	}
	
	/**
	 * This method clears the list of nodes in the nodeList field.
	 * 
	 * @post | Node.nodelist.isEmpty() == true
	 */
	public static void clearNodeList() {
		Node.nodeList.clear();
	}
	
	/**
	 * Returns the path to get to this node using it's parent nodes.
	 * Path is returned IN REVERSE ORDER !!!
	 * 
	 * @return  | List<Node> pathList.add(this) 
	 *          | && if(parent != null) {pathList.addAll(parent.getPath())}
	 *          | result == pathList
	 */
	public List<Node> getPath(){
		List<Node> pathList = new ArrayList<Node>();
		pathList.add(this);
		Node parent = this.getParentNode();
		if(parent!=null){
			pathList.addAll(parent.getPath());
		}
		return pathList;
	}
}
